/******************************************************************************
* Copyright (C) Ultraleap, Inc. 2011-2021. *
* *
* Use subject to the terms of the Apache License 2.0 available at *
* http://www.apache.org/licenses/LICENSE-2.0, or another agreement *
* between Ultraleap and you, your company or other organization. *
******************************************************************************/
using Leap.Unity;
using Leap.Unity.Animation;
using Leap.Unity.Interaction;
using System.Collections.Generic;
using UnityEngine;
namespace Leap.InteractionEngine.Examples
{
///
/// This example script constructs behavior for a very specific kind of UI object that can
/// animate out a "workstation" panel when thrown or placed outside of an anchor.
///
/// Workstation objects exhibit the following behaviors:
/// - Play a tween forwards or backwards when entering or exiting "workstation mode",
/// to open or close an arbitrary transform containing other UI elements.
/// - Smoothly fly to a "good" target location and orientation and enter workstation mode
/// when the user throws the anchorable object.
/// - Set the anchorable object to kinematic when in workstation mode.
/// - Allow the user to adjust the anchorable object's position and rotation in a constrained
/// way while in workstation mode. If the user moves the anchorable object quickly, exit
/// workstation mode.
///
/// It is a more complicated example that demonstrates using InteractionBehaviours and
/// AnchorableBehaviours to create novel UI behavior.
///
[RequireComponent(typeof(InteractionBehaviour))]
[RequireComponent(typeof(AnchorableBehaviour))]
[AddComponentMenu("")]
public class WorkstationBehaviourExample : MonoBehaviour
{
///
/// If the rigidbody of this object moves faster than this speed and the object
/// is in workstation mode, it will exit workstation mode.
///
public const float MAX_SPEED_AS_WORKSTATION = 0.005F;
///
/// If the rigidbody of this object moves slower than this speed and the object
/// wants to enter workstation mode, it will first pick a target position and
/// travel there. (Otherwise, it will open its workstation in-place.)
///
public const float MIN_SPEED_TO_ACTIVATE_TRAVELING = 0.5F;
public TransformTweenBehaviour workstationModeTween;
private InteractionBehaviour _intObj;
private AnchorableBehaviour _anchObj;
private bool _wasKinematicBeforeActivation = false;
///
/// Gets whether the workstation is currently traveling to a target position to open
/// in workstation mode. Will return false if it is not traveling due to not being
/// in workstation mode or if it has already reached its target position.
///
public bool isTraveling { get { return _travelTween.isValid && _travelTween.isRunning; } }
public enum WorkstationState { Closed, Traveling, Opening, Open, Closing }
public WorkstationState workstationState
{
get
{
if (workstationModeTween == null)
return WorkstationState.Closed;
else
{
if (workstationModeTween.tween.progress == 0F && !isTraveling)
{
return WorkstationState.Closed;
}
else if (isTraveling)
{
return WorkstationState.Traveling;
}
else if (workstationModeTween.tween.progress != 1F
&& workstationModeTween.tween.direction == Direction.Forward)
{
return WorkstationState.Opening;
}
else if (workstationModeTween.tween.progress == 1F)
{
return WorkstationState.Open;
}
else
{
return WorkstationState.Closing;
}
}
}
}
void OnValidate()
{
refreshRequiredComponents();
}
void Awake()
{
initWorkstationPoseFunctions();
}
void Start()
{
refreshRequiredComponents();
if (!_anchObj.tryAnchorNearestOnGraspEnd)
{
Debug.LogWarning("WorkstationBehaviour expects its AnchorableBehaviour's tryAnchorNearestOnGraspEnd property to be enabled.", this.gameObject);
}
}
void OnDestroy()
{
_intObj.OnGraspedMovement -= onGraspedMovement;
_anchObj.OnPostTryAnchorOnGraspEnd -= onPostObjectGraspEnd;
}
public void ActivateWorkstation()
{
if (workstationState != WorkstationState.Open)
{
_wasKinematicBeforeActivation = _intObj.rigidbody.isKinematic;
_intObj.rigidbody.isKinematic = true;
}
workstationModeTween.PlayForward();
}
public void DeactivateWorkstation()
{
_intObj.rigidbody.isKinematic = _wasKinematicBeforeActivation;
workstationModeTween.PlayBackward();
}
private void refreshRequiredComponents()
{
_intObj = GetComponent();
_anchObj = GetComponent();
_intObj.OnGraspedMovement += onGraspedMovement;
_anchObj.OnPostTryAnchorOnGraspEnd += onPostObjectGraspEnd;
}
private void onGraspedMovement(Vector3 preSolvePos, Quaternion preSolveRot,
Vector3 curPos, Quaternion curRot,
List controllers)
{
// If the workstation is not fully open when grasped, close it.
if (workstationState == WorkstationState.Opening)
{
DeactivateWorkstation();
}
// If the velocity of the object while grasped is too large, exit workstation mode.
if (_intObj.rigidbody.velocity.magnitude > MAX_SPEED_AS_WORKSTATION
|| (_intObj.rigidbody.isKinematic && ((preSolvePos - curPos).magnitude / Time.fixedDeltaTime) > MAX_SPEED_AS_WORKSTATION))
{
DeactivateWorkstation();
}
// Lock our upward axis while workstation mode is open.
if (workstationState == WorkstationState.Open)
{
_intObj.rigidbody.rotation = (Quaternion.FromToRotation(_intObj.rigidbody.rotation * Vector3.up, Vector3.up)) * _intObj.rigidbody.rotation;
_intObj.transform.rotation = _intObj.rigidbody.rotation;
}
}
private void onPostObjectGraspEnd()
{
if (_anchObj.preferredAnchor == null)
{
// Choose a good position and rotation for workstation mode and begin traveling there.
Vector3 targetPosition;
// Choose the current position if our velocity is small.
if (_intObj.rigidbody.velocity.magnitude < MIN_SPEED_TO_ACTIVATE_TRAVELING)
{
targetPosition = _intObj.rigidbody.position;
}
else
{
targetPosition = determineWorkstationPosition();
}
Quaternion targetRotation = determineWorkstationRotation(targetPosition);
beginTraveling(_intObj.rigidbody.position, _intObj.rigidbody.velocity,
_intObj.rigidbody.rotation, _intObj.rigidbody.angularVelocity,
targetPosition, targetRotation);
}
else
{
// Ensure the workstation is not active or being deactivated if
// we are attaching to an anchor.
DeactivateWorkstation();
}
}
#region Traveling
private const float MAX_TRAVEL_SPEED = 4.00F;
private Tween _travelTween;
private float _initTravelTime = 0F;
private Vector3 _initTravelPosition = Vector3.zero;
private Vector3 _initTravelVelocity = Vector3.zero;
private Quaternion _initTravelRotation = Quaternion.identity;
private Vector3 _initTravelAngVelocity = Vector3.zero;
private Vector3 _effGravity = Vector3.zero;
private Vector3 _travelTargetPosition;
private Quaternion _travelTargetRotation;
private Vector2 _minMaxWorkstationTravelTime = new Vector2(0.05F, 1.00F);
private Vector2 _minMaxTravelTimeFromThrowSpeed = new Vector2(0.30F, 8.00F);
private void beginTraveling(Vector3 initPosition, Vector3 initVelocity,
Quaternion initRotation, Vector3 initAngVelocity,
Vector3 targetPosition, Quaternion targetRotation)
{
_initTravelTime = Time.time;
_initTravelPosition = initPosition;
_initTravelVelocity = initVelocity;
_initTravelRotation = initRotation;
_initTravelAngVelocity = initAngVelocity;
float velMagnitude = _initTravelVelocity.magnitude;
if (velMagnitude > MAX_TRAVEL_SPEED)
{
float capSpeedMultiplier = MAX_TRAVEL_SPEED / velMagnitude;
_initTravelVelocity *= capSpeedMultiplier;
}
_effGravity = Vector3.Lerp(Vector3.zero, Physics.gravity, initVelocity.magnitude.Map(0.80F, 3F, 0F, 0.70F));
_travelTargetPosition = targetPosition;
_travelTargetRotation = targetRotation;
// Construct a single-use Tween that will last a specific duration
// and specify a custom callback as it progresses to update the
// object's position and rotation.
_travelTween = Tween.Single()
.OverTime(_initTravelVelocity.magnitude.Map(_minMaxTravelTimeFromThrowSpeed.x, _minMaxTravelTimeFromThrowSpeed.y,
_minMaxWorkstationTravelTime.x, _minMaxWorkstationTravelTime.y))
.OnProgress(onTravelTweenProgress)
.OnReachEnd(ActivateWorkstation) // When the tween is finished, open workstation mode.
.Play();
}
private void onTravelTweenProgress(float progress)
{
float curTime = Time.time;
Vector3 extrapolatedPosition = evaluatePosition(_initTravelPosition, _initTravelVelocity, _effGravity, _initTravelTime, curTime);
Quaternion extrapolatedRotation = evaluateRotation(_initTravelRotation, _initTravelAngVelocity, _initTravelTime, curTime);
// Interpolate from the position and rotation that the object would naturally have over time
// (by movement due to inertia and by acceleration due to gravity)
// to the target position and rotation over the lifetime of the tween.
_intObj.rigidbody.position = Vector3.Lerp(extrapolatedPosition, _travelTargetPosition, progress);
_intObj.rigidbody.rotation = Quaternion.Slerp(extrapolatedRotation, _travelTargetRotation, progress);
}
private void cancelTraveling()
{
// In case traveling was halted mid-travel-tween, halt the tween.
if (_travelTween.isValid)
{ _travelTween.Stop(); }
}
///
/// Evaluates the position of a body over time with initial velocity and acceleration due to gravity.
///
private Vector3 evaluatePosition(Vector3 initialPosition, Vector3 initialVelocity, Vector3 gravity, float initialTime, float timeToEvaluate)
{
float t = timeToEvaluate - initialTime;
return initialPosition + (initialVelocity * t) + (0.5f * gravity * t * t);
}
///
/// Evaluates the rotation of a body over time with initial velocity and acceleration due to gravity.
///
private Quaternion evaluateRotation(Quaternion initialRotation, Vector3 angularVelocity, float initialTime, float timeToEvaluate)
{
float t = timeToEvaluate - initialTime;
return Quaternion.Euler(angularVelocity * t) * initialRotation;
}
#endregion
#region Workstation Pose
public delegate Vector3 WorkstationPositionFunc(Vector3 userEyePosition, Quaternion userEyeRotation,
Vector3 workstationObjInitPosition, Vector3 workstationObjInitVelocity, float workstationObjRadius,
List otherWorkstationPositions, List otherWorkstationRadii);
public delegate Quaternion WorkstationRotationFunc(Vector3 userEyePosition, Vector3 targetWorkstationPosition);
///
/// The function used to calculate this workstation's target position. By default, will attempt to choose
/// a nearby position approximately in front of the user and in the direction the workstation object is currently
/// traveling, and a position that doesn't overlap with other workstations. The default method is set on Awake(),
/// so it can be overridden in OnEnable() or Start().
///
public WorkstationPositionFunc workstationPositionFunc;
///
/// The function used to calculate this workstation's target rotation. By default, will make the workstation's
/// forward vector face the camera while aligning its upward vector against gravity.
///
public WorkstationRotationFunc workstationRotationFunc;
private List _otherStationObjPositions = new List();
private List _otherStationObjRadii = new List();
// Called on Awake(); possible to override the default functions in a MonoBehaviour's Start().
private void initWorkstationPoseFunctions()
{
workstationPositionFunc = DefaultDetermineWorkstationPosition;
workstationRotationFunc = DefaultDetermineWorkstationRotation;
}
private Vector3 determineWorkstationPosition()
{
return workstationPositionFunc(Camera.main.transform.position, Camera.main.transform.rotation,
_intObj.rigidbody.position, _intObj.rigidbody.velocity, 0.30F,
_otherStationObjPositions, _otherStationObjRadii);
}
private Quaternion determineWorkstationRotation(Vector3 workstationPosition)
{
return workstationRotationFunc(Camera.main.transform.position, workstationPosition);
}
public static Vector3 DefaultDetermineWorkstationPosition(Vector3 userEyePosition, Quaternion userEyeRotation,
Vector3 workstationObjInitPosition, Vector3 workstationObjInitVelocity, float workstationObjRadius,
List otherWorkstationPositions, List otherWorkstationRadii)
{
// Push velocity away from the camera if necessary.
Vector3 towardsCamera = (userEyePosition - workstationObjInitPosition).normalized;
float towardsCameraness = Mathf.Clamp01(Vector3.Dot(towardsCamera, workstationObjInitVelocity.normalized));
workstationObjInitVelocity = workstationObjInitVelocity + Vector3.Lerp(Vector3.zero, -towardsCamera * 2.00F, towardsCameraness);
// Calculate velocity direction on the XZ plane.
Vector3 groundPlaneVelocity = Vector3.ProjectOnPlane(workstationObjInitVelocity, Vector3.up);
float groundPlaneDirectedness = groundPlaneVelocity.magnitude.Map(0.003F, 0.40F, 0F, 1F);
Vector3 groundPlaneDirection = groundPlaneVelocity.normalized;
// Calculate camera "forward" direction on the XZ plane.
Vector3 cameraGroundPlaneForward = Vector3.ProjectOnPlane(userEyeRotation * Vector3.forward, Vector3.up);
float cameraGroundPlaneDirectedness = cameraGroundPlaneForward.magnitude.Map(0.001F, 0.01F, 0F, 1F);
Vector3 alternateCameraDirection = (userEyeRotation * Vector3.forward).y > 0F ? userEyeRotation * Vector3.down : userEyeRotation * Vector3.up;
cameraGroundPlaneForward = Vector3.Slerp(alternateCameraDirection, cameraGroundPlaneForward, cameraGroundPlaneDirectedness);
cameraGroundPlaneForward = cameraGroundPlaneForward.normalized;
// Calculate a placement direction based on the camera and throw directions on the XZ plane.
Vector3 placementDirection = Vector3.Slerp(cameraGroundPlaneForward, groundPlaneDirection, groundPlaneDirectedness);
// Calculate a placement position along the placement direction between min and max placement distances.
float minPlacementDistance = 0.25F;
float maxPlacementDistance = 0.51F;
Vector3 placementPosition = userEyePosition + placementDirection * Mathf.Lerp(minPlacementDistance, maxPlacementDistance,
(groundPlaneDirectedness * workstationObjInitVelocity.magnitude)
.Map(0F, 1.50F, 0F, 1F));
// Don't move far if the initial velocity is small.
float overallDirectedness = workstationObjInitVelocity.magnitude.Map(0.00F, 3.00F, 0F, 1F);
placementPosition = Vector3.Lerp(workstationObjInitPosition, placementPosition, overallDirectedness * overallDirectedness);
// Enforce placement height.
float placementHeightFromCamera = -0.30F;
placementPosition.y = userEyePosition.y + placementHeightFromCamera;
// Enforce minimum placement away from user.
Vector2 cameraXZ = new Vector2(userEyePosition.x, userEyePosition.z);
Vector2 stationXZ = new Vector2(placementPosition.x, placementPosition.z);
float placementDist = Vector2.Distance(cameraXZ, stationXZ);
if (placementDist < minPlacementDistance)
{
float distanceLeft = (minPlacementDistance - placementDist) + workstationObjRadius;
Vector2 xzDisplacement = (stationXZ - cameraXZ).normalized * distanceLeft;
placementPosition += new Vector3(xzDisplacement[0], 0F, xzDisplacement[1]);
}
return placementPosition;
}
public static Quaternion DefaultDetermineWorkstationRotation(Vector3 userEyePos, Vector3 workstationPosition)
{
Vector3 toCamera = userEyePos - workstationPosition;
toCamera.y = 0F;
Quaternion placementRotation = Quaternion.LookRotation(toCamera.normalized, Vector3.up);
return placementRotation;
}
#endregion
}
}